VitePress + GitHub Actions:私有笔记仓库自动发布到 GitHub Pages
Markdown 笔记放在私有仓库里,希望自动构建成静态站点发布到 GitHub Pages:源文件保密、只公开构建产物、推送即部署。本文给出一套完整方案,覆盖 VitePress 配置、自动侧边栏生成、GitHub Actions 跨仓库部署与常见问题。适合想把已有笔记仓库变成个人站点的开发者,读完可以直接照配出一个「推送即发布」的笔记站点。
方案架构
私有仓库只存放源文件,构建在 GitHub Actions 里完成,产物推送到公开仓库,由 GitHub Pages 对外发布:
这个架构有几个特点:
- 源文件保密:私有仓库不对外可见,只有构建结果公开;
- 自动部署:推送代码即触发构建和发布,无需本地操作;
- 选择性发布:私密目录(草稿、工作记录等)可以通过配置排除在构建之外;
- 内置能力:VitePress 自带本地全文搜索、响应式布局与自动生成的侧边栏。
环境要求
- Node.js:官方文档当前要求 22 及以上(稳定版 1.6.4;早期 1.x 版本要求 18 以上,如果
package.json里写的是^1.0.0,实际会安装到 1.6.x,因此新搭建建议直接用 22 LTS); - npm:9.0.0 及以上;
- Git:2.0 及以上。
node -v
npm -v
git -v Node.js 多版本切换可以参考 nvm、n 与 Volta 三类版本管理器,此处不展开。
项目配置
目录结构
一个可用的笔记仓库大致长这样:
note-repo/
├── .github/
│ └── workflows/
│ └── deploy.yml # GitHub Actions 部署配置
├── .vitepress/
│ ├── config.mts # VitePress 主配置
│ ├── utils/
│ │ └── sidebar.ts # 自动侧边栏生成工具
│ ├── dist/ # 构建输出目录(自动生成)
│ └── cache/ # 缓存目录(自动生成)
├── notes/ # 内容目录,每个目录放一个 index.md
├── draft/ # 私密目录(构建时排除)
├── pic/ # 图片资源目录
├── index.md # 网站首页
├── package.json
└── .gitignore package.json
{
"name": "note",
"version": "1.0.0",
"type": "module",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.0.0"
},
"engines": {
"node": ">=22.0.0",
"npm": ">=9.0.0"
}
} type: "module" 启用 ES 模块支持,engines 声明版本要求,scripts 定义三个常用命令。
.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { fileURLToPath, URL } from 'node:url'
import { generateSidebar } from './utils/sidebar'
// .vitepress 目录路径
const rootDir = fileURLToPath(new URL('.', import.meta.url))
export default defineConfig({
title: '个人笔记',
description: '记录工作学习笔记',
lang: 'zh-CN',
// 部署到 https://username.github.io/ 用 '/';
// 部署到 https://username.github.io/repo/ 用 '/repo/'
base: '/',
// 笔记互链常有站外链接,关掉死链检查避免构建失败
ignoreDeadLinks: true,
lastUpdated: true,
// 私密目录不参与构建
srcExclude: ['**/draft/**', '**/node_modules/**'],
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '笔记', link: '/notes/' }
],
// 侧边栏按目录自动生成
sidebar: {
'/notes/': [
{
text: '笔记',
items: generateSidebar('notes', rootDir)
}
]
},
// 本地全文搜索(中文化)
search: {
provider: 'local',
options: {
locales: {
root: {
translations: {
button: { buttonText: '搜索笔记', buttonAriaLabel: '搜索笔记' },
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: { selectText: '选择', navigateText: '切换' }
}
}
}
}
}
},
lastUpdated: { text: '最后更新于' },
docFooter: { prev: '上一页', next: '下一页' },
outline: { label: '页面导航', level: [2, 3] },
returnToTopLabel: '返回顶部',
sidebarMenuLabel: '菜单',
darkModeSwitchLabel: '外观'
},
markdown: {
lineNumbers: true,
image: { lazyLoading: true }
}
}) 几个选项值得说明:
base决定资源引用前缀,用户名站点(username.github.io)用/,项目站点(username.github.io/repo/)必须写/repo/,否则样式与图片全部 404;ignoreDeadLinks建议开启,笔记里的相对链接经常在构建时被判为死链;srcExclude用 glob 语法排除私密目录,这是「源仓库可以放任何东西、站点只发布选定部分」的关键;search.provider: 'local'是 VitePress 内置的本地索引搜索,不需要接 Algolia 等外部服务。
自动侧边栏
侧边栏如果手写,每加一篇笔记都要改配置。可以用一个递归函数在构建时扫描目录自动生成:
// .vitepress/utils/sidebar.ts
import { readdirSync, statSync } from 'fs'
import { join } from 'path'
export interface SidebarItem {
text: string
link?: string
items?: SidebarItem[]
collapsed?: boolean
}
function walk(dir: string, linkBase: string): SidebarItem[] {
const items: SidebarItem[] = []
const files = readdirSync(dir)
// 目录在前、文件在后,同级按中文名排序
files.sort((a, b) => {
const aDir = statSync(join(dir, a)).isDirectory()
const bDir = statSync(join(dir, b)).isDirectory()
if (aDir !== bDir) return aDir ? -1 : 1
return a.localeCompare(b, 'zh-CN')
})
for (const file of files) {
// 跳过隐藏文件与下划线开头的文件
if (file.startsWith('.') || file.startsWith('_')) continue
const full = join(dir, file)
if (statSync(full).isDirectory()) {
const sub = walk(full, `${linkBase}/${file}`)
if (sub.length > 0) items.push({ text: file, collapsed: true, items: sub })
} else if (file.endsWith('.md') && file !== 'index.md') {
items.push({ text: file.replace('.md', ''), link: `${linkBase}/${file}` })
}
}
return items
}
// dir:相对站点根的目录名;rootDir:.vitepress 目录
export function generateSidebar(dir: string, rootDir: string): SidebarItem[] {
return walk(join(rootDir, '..', dir), `/${dir}`)
} 子目录会折叠展示,index.md 被跳过(它承担目录页职责,不进列表)。目录层级变化时侧边栏自动跟上,不需要维护任何清单。
首页与目录页
首页 index.md 用 VitePress 的 home 布局:
---
layout: home
hero:
name: "个人笔记"
text: "记录工作学习的点点滴滴"
tagline: 持续学习,不断进步
actions:
- theme: brand
text: 开始阅读
link: /notes/
---
## 关于本站
个人学习笔记站点,使用 VitePress 构建。 每个内容目录放一个 index.md(可以只有一行标题),否则访问该目录路径会 404。
自动化部署
创建 Personal Access Token
构建产物要推送到另一个公开仓库,需要给 Actions 授权:
- GitHub Settings → Developer settings → Personal access tokens → Tokens (classic);
- Generate new token (classic),勾选
repo完整权限,设置较长有效期; - 生成后立即复制,token 只显示一次。
配置 Secret
在私有仓库的 Settings → Secrets and variables → Actions 中新建 secret,名称 PERSONAL_ACCESS_TOKEN,值为刚才的 token。secret 注入环境变量时对日志脱敏,不会泄漏。
工作流
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [master]
workflow_dispatch: # 支持手动触发
permissions:
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 完整历史,用于 lastUpdated 时间
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build with VitePress
run: npm run docs:build
env:
NODE_ENV: production
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
external_repository: username/username.github.io # 目标公开仓库
publish_branch: main
publish_dir: .vitepress/dist
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
force_orphan: true # 每次全量覆盖,不带历史 要点:
permissions: contents: read最小化本仓库权限,跨仓库推送靠 PAT 完成;node-version: 22与前述 VitePress 版本要求对应;force_orphan: true让公开仓库始终只有一次提交,历史不会膨胀。
配置 GitHub Pages
在公开仓库(username.github.io)的 Settings → Pages 中,Source 选 Deploy from a branch,Branch 选 main、目录 / (root)。等待一两分钟即可通过 https://username.github.io/ 访问。
本地开发
git clone https://github.com/username/note-repo.git
cd note-repo
npm install
npm run docs:dev # 开发模式,默认 http://localhost:5173
npm run docs:build # 构建到 .vitepress/dist
npm run docs:preview # 本地预览构建结果 本地预览通过后再推送,推送后 Actions 自动完成发布。
常见问题
构建报 Duplicate attribute。 多数是 Markdown 文件的 front matter 开头多了空行——--- 必须是文件第一行。批量排查:
grep -rln $'^---\n\n' --include="*.md" . 访问目录 404。 该目录缺少 index.md,补一个即可。
部署报 Permission denied(git exit code 128)。 PAT 过期或权限不足:确认 token 未过期、勾选了 repo 权限,必要时重新生成并更新 secret。
代码块语言不识别。 VitePress 内置 Shiki 不认识 plantuml 等语言时会回退为纯文本并告警,把语言标注换成 text 或 mermaid 即可。
私密目录出现在站点上。 检查 srcExclude 的 glob 是否覆盖了该目录(注意 **/draft/** 与 draft/** 的区别),改完清缓存重新构建:
rm -rf .vitepress/cache .vitepress/dist
npm run docs:build 小结
这套方案的核心是职责分离:私有仓库管源文件,Actions 管构建,公开仓库只承载产物。配置上的三个关键点是 base 与站点路径的对应、srcExclude 排除私密内容、PAT 驱动的跨仓库推送;自动侧边栏则把目录维护成本降到了零。搭好之后,写笔记、提交、推送,站点自动更新。